/**
 * 文件名称: write_log.c
 * 摘    要: linux应用程序下记录日志源文件
 * 来    源: 参考: https://blog.csdn.net/qq_39436605/article/details/81181061
 *
 * 当前版本: 1.0
 * 作    者: huenrong
 * 完成日期: 2019年06月20日
 **/

#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>

#include "write_log.h"

#ifdef ELOG_COLOR_ENABLE
// 终端日志信息颜色定义(依次为: 字体颜色，背景颜色，字体格式)
static const char *g_log_color[4][3] = {
    {F_RED, B_NULL, S_NORMAL},    // LOG_ERROR的日志颜色定义
    {F_YELLOW, B_NULL, S_NORMAL}, // LOG_WARN的日志颜色定义
    {F_CYAN, B_NULL, S_NORMAL},   // LOG_INFO的日志颜色定义
    {F_GREEN, B_NULL, S_NORMAL},  // LOG_DEBUG的日志颜色定义
};
#endif

/************************************************************************
函数名称: get_time
函数功能: 获取时间串(时间串样式: YYYY-MM-DD HH:MIN:SS.Usec)
函数参数: get_time_str: 获取到的时间串
          flag: 0: 时间串样式: [YYYY-MM-DD HH:MIN:SS.Usec]
          1: 时间串样式: YYYY-MM-DD_HH-MIN-SS-Usec
函数返回: 无
************************************************************************/
static void get_time(unsigned char *get_time_str, const unsigned char flag)
{
    struct tm sys_time = {0};
    time_t current_time = {0};
    struct timeval get_time_value = {0};

    unsigned char microsecond[20] = {0}; // 微秒
    unsigned char millisecond[20] = {0}; // 毫秒

    if (NULL == get_time_str)
    {
        return;
    }

    current_time = time(NULL);
    localtime_r(&current_time, &sys_time); // localtime_r是线程安全的

    gettimeofday(&get_time_value, NULL);
    sprintf((char *)microsecond, "%ld", get_time_value.tv_usec); // 获取微秒
    strncpy((char *)millisecond, (const char *)microsecond, 3);  // 微秒的前3位为毫秒(1毫秒=1000微秒)

    if (0 == flag)
    {
        sprintf((char *)get_time_str, "[%04d-%02d-%02d %02d:%02d:%02d.%3.3s]",
                sys_time.tm_year + 1900, sys_time.tm_mon + 1, sys_time.tm_mday,
                sys_time.tm_hour, sys_time.tm_min, sys_time.tm_sec, millisecond);
    }
    else if (1 == flag)
    {
        sprintf((char *)get_time_str, "%04d-%02d-%02d_%02d-%02d-%02d-%3.3s",
                sys_time.tm_year + 1900, sys_time.tm_mon + 1, sys_time.tm_mday,
                sys_time.tm_hour, sys_time.tm_min, sys_time.tm_sec, millisecond);
    }
}

/************************************************************************
函数名称: get_log_level
函数功能: 根据日志等级获取对应字符串
函数参数: log_level: 日志等级
函数返回: 日志等级信息字符串
************************************************************************/
static char *get_log_level(const unsigned char log_level)
{
    switch (log_level)
    {
    case LOG_ERROR:
    {
        return "ERROR";
    }

    case LOG_WARN:
    {
        return "WARN";
    }

    case LOG_INFO:
    {
        return "INFO";
    }

    case LOG_DEBUG:
    {
        return "DEBUG";
    }

    default:
    {
        return "OTHER";
    }
    }
}

/************************************************************************
函数名称: get_file_lines
函数功能: 获取指定文件的行数
函数参数: file_name: 文件名
函数返回: 成功: 返回当前文件的行数
          失败: 返回-1
************************************************************************/
static unsigned int get_file_lines(const char *file_name)
{
    FILE *fstream = NULL;
    char cmd_buf[56] = {0}; // 文件名(包括文件扩展名)长度最大为50
    char out_buf[60] = {0}; // 命令输出结果buf
    unsigned int line = 0;  // 返回的文件行数

    sprintf(cmd_buf, "wc -l %s", file_name);

    if ((fstream = popen(cmd_buf, "r"))) // fork子进程执行命令
    {
        memset(out_buf, 0x00, sizeof(out_buf));
        if (fgets(out_buf, sizeof(out_buf), fstream)) // 获取命令输出结果
        {
            pclose(fstream); // 关闭popen fork的子进程

            line = strchr((const char *)out_buf, ' ') - out_buf; // 获取长度, 第一次出现空格的位置
            out_buf[line] = '\0';
            line = atoi(out_buf); // 计算文件行数

            return line;
        }
    }

    if (fstream)
    {
        pclose(fstream);
    }

    return -1;
}

/************************************************************************
函数名称: __printf_log_error
函数功能: 打印错误日志(打印代码文件名、函数名、代码所在行)
函数参数: file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          fmt: 要打印内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __printf_log_error(const char *file_name, const char *function_name,
                        const unsigned int code_line, const char *fmt, ...)
{
    unsigned char get_time_str[30] = {0}; // 获取到的时间字符串
    va_list va;
    char log_buf[1024] = {0};

    // 如果当前函数输出等级大于可输出等级，则直接不输出
    if (LOG_ERROR > LOG_LEVEL)
    {
        return;
    }

    // 获取当前时间戳
    memset(get_time_str, 0, sizeof(get_time_str));
    get_time(get_time_str, 0);

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    // 要打印的日志长度为0, 则直接退出
    if (0 == strlen(log_buf))
    {
        return;
    }

#ifdef ELOG_COLOR_ENABLE
    printf("%s%s%s%s%s [%s] [%s] [%04d]: %s%s",
           CSI_START, g_log_color[0][0], g_log_color[0][1], g_log_color[0][2],
           get_time_str, file_name, function_name, code_line, log_buf, CSI_END);
#else
    printf("%s [%s] [%s] [%04d]: %s", get_time_str, file_name, function_name, code_line, log_buf);
#endif
}

/************************************************************************
函数名称: __printf_log_warn
函数功能: 打印警告日志(打印代码文件名、函数名、代码所在行)
函数参数: file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          fmt: 要打印内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __printf_log_warn(const char *file_name, const char *function_name,
                       const unsigned int code_line, const char *fmt, ...)
{
    unsigned char get_time_str[30] = {0}; // 获取到的时间字符串
    va_list va;
    char log_buf[1024] = {0};

    // 如果当前函数输出等级大于可输出等级，则直接不输出
    if (LOG_WARN > LOG_LEVEL)
    {
        return;
    }

    // 获取当前时间戳
    memset(get_time_str, 0, sizeof(get_time_str));
    get_time(get_time_str, 0);

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    // 要打印的日志长度为0, 则直接退出
    if (0 == strlen(log_buf))
    {
        return;
    }

#ifdef ELOG_COLOR_ENABLE
    printf("%s%s%s%s%s [%s] [%s] [%04d]: %s%s",
           CSI_START, g_log_color[1][0], g_log_color[1][1], g_log_color[1][2],
           get_time_str, file_name, function_name, code_line, log_buf, CSI_END);
#else
    printf("%s [%s] [%s] [%04d]: %s", get_time_str, file_name, function_name, code_line, log_buf);
#endif
}

/************************************************************************
函数名称: __printf_log_info
函数功能: 打印信息日志(打印代码文件名、函数名、代码所在行)
函数参数: file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          fmt: 要打印内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __printf_log_info(const char *file_name, const char *function_name,
                       const unsigned int code_line, const char *fmt, ...)
{
    unsigned char get_time_str[30] = {0}; // 获取到的时间字符串
    va_list va;
    char log_buf[1024] = {0};

    // 如果当前函数输出等级大于可输出等级，则直接不输出
    if (LOG_INFO > LOG_LEVEL)
    {
        return;
    }

    // 获取当前时间戳
    memset(get_time_str, 0, sizeof(get_time_str));
    get_time(get_time_str, 0);

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    // 要打印的日志长度为0, 则直接退出
    if (0 == strlen(log_buf))
    {
        return;
    }

#ifdef ELOG_COLOR_ENABLE
    printf("%s%s%s%s%s [%s] [%s] [%04d]: %s%s",
           CSI_START, g_log_color[2][0], g_log_color[2][1], g_log_color[2][2],
           get_time_str, file_name, function_name, code_line, log_buf, CSI_END);
#else
    printf("%s [%s] [%s] [%04d]: %s", get_time_str, file_name, function_name, code_line, log_buf);
#endif
}

/************************************************************************
函数名称: __printf_log_debug
函数功能: 打印调试日志(打印代码文件名、函数名、代码所在行)
函数参数: file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          fmt: 要打印内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __printf_log_debug(const char *file_name, const char *function_name,
                        const unsigned int code_line, const char *fmt, ...)
{
    unsigned char get_time_str[30] = {0}; // 获取到的时间字符串
    va_list va;
    char log_buf[1024] = {0};

    // 如果当前函数输出等级大于可输出等级，则直接不输出
    if (LOG_DEBUG > LOG_LEVEL)
    {
        return;
    }

    // 获取当前时间戳
    memset(get_time_str, 0, sizeof(get_time_str));
    get_time(get_time_str, 0);

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    // 要打印的日志长度为0, 则直接退出
    if (0 == strlen(log_buf))
    {
        return;
    }

#ifdef ELOG_COLOR_ENABLE
    printf("%s%s%s%s%s [%s] [%s] [%04d]: %s%s",
           CSI_START, g_log_color[3][0], g_log_color[3][1], g_log_color[3][2],
           get_time_str, file_name, function_name, code_line, log_buf, CSI_END);
#else
    printf("%s [%s] [%s] [%04d]: %s", get_time_str, file_name, function_name, code_line, log_buf);
#endif
}

/************************************************************************
函数名称: __printf_log
函数功能: 打印日志(打印代码文件名、函数名、代码所在行)
函数参数: log_level: 需要打印的日志等级
          file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          fmt: 要打印内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __printf_log(const char *file_name, const char *function_name, const unsigned int code_line,
                  const unsigned char log_level, const char *fmt, ...)
{
    va_list va;
    char log_buf[1024] = {0};

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    // 要打印的日志长度为0, 则直接退出
    if (0 == strlen(log_buf))
    {
        return;
    }

    switch (log_level)
    {
    case LOG_ERROR:
    {
        __printf_log_error(file_name, function_name, code_line, log_buf);

        break;
    }

    case LOG_WARN:
    {
        __printf_log_warn(file_name, function_name, code_line, log_buf);

        break;
    }

    case LOG_INFO:
    {
        __printf_log_info(file_name, function_name, code_line, log_buf);

        break;
    }

    case LOG_DEBUG:
    {
        __printf_log_debug(file_name, function_name, code_line, log_buf);

        break;
    }

    default:
        break;
    }
}

/************************************************************************
函数名称: __write_log_file
函数功能: 将内容写到日志文件中(记录代码文件名、函数名、代码所在行)
函数参数: file_name: 代码文件名
          function_name: 代码所在函数名
          code_line: 代码行
          log_level: 日志等级
          fmt: 要写入内容的指针
          ...: 其他参数
函数返回: 无
************************************************************************/
void __write_log_file(const char *file_name, const char *function_name, const unsigned int code_line,
                      const unsigned char log_level, const char *fmt, ...)
{
    FILE *fp = NULL;
    int ret = -1;
    static unsigned char run_num_flag = 0;    // 函数运行次数标志位
    static char log_file_name[100] = {0};     // 日志文件名(静态变量, 便于下次记录仍旧知道文件名)
    unsigned int lines = 0;                   // 获取到的日志文件行数
    unsigned char get_time_str[30] = {0};     // 获取到的时间字符串
    unsigned char write_log_data[2048] = {0}; // 最终写入日志文件的内容
    char log_dir[60] = {0};                   // 日志文件路径前缀 + 日期文件夹名
    char date_buf[15] = {0};                  // 获取到的日期
    va_list va;
    char log_buf[1024] = {0};

    // 获取变参
    va_start(va, fmt);
    memset(log_buf, 0, sizeof(log_buf));
    (void)vsprintf(log_buf, fmt, va);
    va_end(va);

    if ((NULL == file_name) || (0 == strlen(log_buf)))
    {
        return;
    }

    if (log_level > LOG_LEVEL) // 过滤日志等级
    {
        return;
    }

    ret = access(LOG_FILE_PATH, 0); // 判断是否存在指定的路径(存储日志路径)
    if (-1 == ret)
    {
        char create_log_file_path[256] = {0}; // 创建目录命令
        // 指定目录权限为755
        sprintf(create_log_file_path, "mkdir -p -m 755 %s", LOG_FILE_PATH);
        system(create_log_file_path);
    }

    if (0 == run_num_flag) // 函数是第一次运行, 需要创建一个全新的文件
    {
        get_time(get_time_str, 1);
        memset(date_buf, 0, sizeof(date_buf));
        strncpy(date_buf, (const char *)get_time_str, 10); // 获取前面的日期
        memset(log_dir, 0, sizeof(log_dir));
        sprintf(log_dir, "%s/%s", LOG_FILE_PATH, date_buf);
        ret = access(log_dir, 0); // 判断是否存在日期文件夹
        if (-1 == ret)
        {
            mkdir(log_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); // 创建一个日期文件夹
        }

        memset(log_file_name, 0, sizeof(log_file_name));
        sprintf(log_file_name, "%s/%s_%s.log", log_dir, LOG_FILE_NAME_PREFIX, get_time_str);

        run_num_flag = 1;
    }
    else
    {
        get_time(get_time_str, 1);
        memset(date_buf, 0, sizeof(date_buf));
        strncpy(date_buf, (const char *)get_time_str, 10); // 获取前面的日期
        memset(log_dir, 0, sizeof(log_dir));
        sprintf(log_dir, "%s/%s", LOG_FILE_PATH, date_buf);
        ret = access(log_dir, 0); // 判断是否存在正确的日期文件夹
        if (-1 == ret)
        {
            mkdir(log_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); // 创建一个日期文件夹

            // 更新日志文件名
            memset(log_file_name, 0, sizeof(log_file_name));
            sprintf(log_file_name, "%s/%s_%s.log", log_dir, LOG_FILE_NAME_PREFIX, get_time_str);
        }
        else // 文件夹下面有正确的日期文件夹, 则继续判断上次的文件名行数是否超限
        {
            lines = get_file_lines(log_file_name);
            if (lines >= LOG_FILE_MAX_LINE) // 如果文件行数超过限制, 则重新换一个文件记录日志
            {
                memset(log_file_name, 0, sizeof(log_file_name));
                sprintf(log_file_name, "%s/%s_%s.log", log_dir, LOG_FILE_NAME_PREFIX, get_time_str);
            }
        }
    }

    fp = fopen(log_file_name, "at+"); // 打开文件, 每次写入的时候在后面追加
    if (NULL == fp)
    {
        return;
    }

    // 写入日志时间
    memset(get_time_str, 0, sizeof(get_time_str));
    get_time(get_time_str, 0);
    fputs((const char *)get_time_str, fp);

    // 写入日志内容
    if (1 == WRITE_LOG_FLAG) // 在日志信息中显示"文件名/函数名/代码行数"信息
    {
        snprintf((char *)write_log_data, sizeof(write_log_data) - 1, " [%s] [%s] [%04d] [%s]: %s\n",
                 file_name, function_name, code_line, get_log_level(log_level), log_buf);
    }
    else // 不用在日志信息中显示"文件名/代码行数"信息
    {
        snprintf((char *)write_log_data, sizeof(write_log_data) - 1, "[%s]%s\n",
                 get_log_level(log_level), log_buf);
    }
    fputs((const char *)write_log_data, fp);

    fflush(fp); // 刷新文件
    fclose(fp); // 关闭文件
    fp = NULL;  // 将文件指针置为空
}
